#include "driverjack.h"
#include <QStringList>
#include <QWaitCondition>
#include <QMutex>
#include <QDebug>
#include "free-cube/logger.h"
#include "free-cube/ahosthandler.h"

//#include "globals.h"
//#include "aiprocessor.h"

using namespace fc;

struct DriverJack::AudioPortStruct {
    Sample * buffer;
    bool isOutput;
    jack_port_t * jackPort;
    QStringList connectedPortsNames;
};

struct DriverJack::StaticData {
    static int 		process(jack_nframes_t nframes, void *unused_arg);
    static int 		srate (jack_nframes_t nframes, void *unused_arg);
    static void 	error (const char *desc);
    static void 	jack_shutdown (void *unused_arg);
    static int 		bufsize_callback (jack_nframes_t  nframes,
                                      void *unused_arg);
    jack_nframes_t 	sampleRate;
    jack_nframes_t 	bufferSize;
    jack_client_t * 	client;
    AudioPortStruct audioPortsList [MAX_N_PORTS];
    int NPortsRegistered;
    QWaitCondition buffersReady;
    bool isFinishing;
    AHostHandler * host_handler_;
};

struct DriverJack::HostThread : public QThread
{
    HostThread(QWaitCondition & wc) :
        buffers_ready_(wc),
        process_(do_nothing_),
        sample_rate_changed_(do_nothing_),
        buffer_size_changed_(do_nothing_),
        connection_changed_(do_nothing_){}
    static void do_nothing_(){}

    QWaitCondition & buffers_ready_;
    void (*process_)();
    void (*sample_rate_changed_)();
    void (*buffer_size_changed_)();
    void (*connection_changed_)();
    void run(); // to implement from QThread
};

DriverJack::StaticData DriverJack::staticData_;

// ====================  STATIC SECTION (CALLBACKS) BEGIN
//
int DriverJack::StaticData::process (jack_nframes_t nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    Sample * tmpBuffer;
    AudioPortStruct * audioPort;
    for (int i = 0; i < MAX_N_PORTS; i++) {
        audioPort = staticData_.audioPortsList + i;
        if (audioPort->buffer != NULL &&
                audioPort->jackPort != NULL) {
            tmpBuffer = (Sample *) jack_port_get_buffer (
                        audioPort->jackPort, nFrames);
            if (audioPort->isOutput) {
                memcpy(tmpBuffer, audioPort->buffer,
                       sizeof(Sample)*nFrames);
            } else {
                memcpy(audioPort->buffer, tmpBuffer,
                       sizeof(Sample)*nFrames);
            }
        }
    }
    staticData_.buffersReady.wakeAll();
    return 0;
}

int DriverJack::StaticData::srate (jack_nframes_t nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    log("JD: now the sample rate is %1", nFrames);
    staticData_.sampleRate = nFrames;
    return 0;
}

void DriverJack::StaticData::error (const char *desc)
{
    log("JD: JACK error: %1", desc);
}

void DriverJack::StaticData::jack_shutdown (void *unused_arg)
{
    Q_UNUSED(unused_arg);
}

int DriverJack::StaticData::bufsize_callback (jack_nframes_t  nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    log("JD: buffer size is now %1", nFrames);
    staticData_.bufferSize = nFrames;
    return 0;
}
//
// ====================  STATIC SECTION (CALLBACKS) END
//
DriverJack::DriverJack(const char * nameOfClient) :
    driverState_(CREATED),
    nameOfClient_(nameOfClient),
    nameOfDriver_ (NAME_OF_DRIVER_JACK),
    host_thread_(new HostThread(staticData_.buffersReady))

{
    staticData_.host_handler_ = NULL;
    staticData_.NPortsRegistered = 0;
    staticData_.bufferSize = 0;
    staticData_.client = NULL;
    staticData_.isFinishing = true;
    log("JD: ...construction ok ");
}

DriverJack::~DriverJack()
{
    log("JD: exiting...");
    if(driverState_ == STARTED) stop();
    log("JD: cleanup...");
    AudioPortStruct * ap; // saving connections of client
    for (int i = 0; i < MAX_N_PORTS; i++) {
        ap = & (staticData_.audioPortsList[i]);
        if (! ap->connectedPortsNames.isEmpty())
            ap->connectedPortsNames.clear();
    }
    log("JD: ...closing client ");
    if (staticData_.client != NULL) jack_client_close (staticData_.client);
    delete host_thread_;
    log("JD: ...deconstruction ok ");
}

void DriverJack::start(){
    registerPort((Sample*) buffer_.output(0), true, "OUT" + QString("L"));
    registerPort((Sample*) buffer_.output(1), true, "OUT" + QString("R"));
    registerPort((Sample*) buffer_.input(0), false, "IN" +  QString("L"));
    registerPort((Sample*) buffer_.input(1), false, "IN" +  QString("R"));
    if (startJack()!=0) return;
    connectDefaultPorts();
    staticData_.isFinishing = false;
    host_thread_->start(); // start processing thread
}

void DriverJack::stop(){
    staticData_.isFinishing = true;
    staticData_.buffersReady.wakeAll();
    sleep(1);
    stopJack();
    clearPorts();
}


int DriverJack::init(AHostHandler & host_handler)
{
    staticData_.host_handler_ = &host_handler;

    if (driverState_ != CREATED) {
        log("JD: cannot init driver - not created yet"); return -1;
    }

    log("JD: initialization started...");
    if (nameOfClient_.size() == 0) nameOfClient_ = "Unknown";
    jack_set_error_function (StaticData::error);
    // open client, register callbacks, actvate client
    if ( 0 == (staticData_.client =
               jack_client_open(nameOfClient_.toAscii().data(),
                                JackNullOption, NULL))) {
        log("JD: jack server not running?\n");
        return -1;
    }
    jack_set_process_callback (staticData_.client,
                               StaticData::process, 0);
    jack_set_sample_rate_callback (staticData_.client,
                                   StaticData::srate, 0);
                            // doesnt work on WINxx should be reconsidered:
                            // jack_on_shutdown (staticData_.client,
                            //              StaticData::jack_shutdown, 0);
                            //    log(QString("..on_shutdown"));
    jack_set_buffer_size_callback (staticData_.client,
                                   StaticData::bufsize_callback, 0);
    staticData_.sampleRate = jack_get_sample_rate (staticData_.client);
    staticData_.bufferSize = jack_get_buffer_size (staticData_.client);
    log("JD: sample rate : %1, buffer size : %2",
        staticData_.sampleRate,
        staticData_.bufferSize);
    for (int i = 0; i < MAX_N_PORTS; i++) {
        staticData_.audioPortsList[i].jackPort = NULL;
        staticData_.audioPortsList[i].buffer = NULL;
    }
    driverState_ = INITED;
    return 0;
}


ReturnCode DriverJack::startJack()
{
    if (! (driverState_ == INITED)) {
        log(QString( "JD: cannot start driver - not inited"));
        return -1;//JACK_CANT_ACTIVATE;
    }
    log("JD: starting jack client...");
    if (staticData_.client == NULL ) {
        log(QString( "JD: cannot activate client"));
        return -1;//JACK_CANT_ACTIVATE;
    }
    if (jack_activate (staticData_.client) == 0) {
        log(QString( "JD: client is activated and started"));
        driverState_ = STARTED;
        return 0;//SUCCESS;
    } else {
        log(QString( "JD: client cant be activated, unknown error")) ;
        return -1;//JACK_CANT_ACTIVATE;
    }
}

ReturnCode DriverJack::stopJack()
{
    if (driverState_ != STARTED) {
        log(QString( "JD: trying to stop driver which has been not started"));
        return -1; // JACK_CANT_ACTIVATE;
    }
    log("JD: stopping jack client...");
    if (staticData_.client == NULL) {
        log(QString( "JD: nothing to deactivate - no client (NULL pointer)"));
        return -1;//JACK_CANT_DEACTIVATE;
    }
    if (jack_deactivate (staticData_.client) == 0) {
        log(QString( "JD: client is deactivated"));
        driverState_ = STOPED;
        return 0; //SUCCESS;
    }
    log(QString( "JD: client can not be deactivated"));
    return -1;//JACK_CANT_DEACTIVATE;
}



void DriverJack::HostThread::run(){  // QThread running function
    log("DJ: thread started...");
    QMutex mutex;
    while (not staticData_.isFinishing){
        mutex.lock();
        buffers_ready_.wait(&mutex);
        staticData_.host_handler_->process();
        mutex.unlock();
    }
    log("DJ: thread stopped...");
}

const QString & DriverJack::name()
{
    return nameOfDriver_;
}

QStringList * DriverJack::getPhysicalPortNames(bool isJackOutput, bool isAudio)
{
    unsigned long jpf =  JackPortIsPhysical |
            (isJackOutput ? JackPortIsOutput : JackPortIsInput);
    const char ** physicalPorts;
    if ((physicalPorts =
         jack_get_ports (staticData_.client, NULL, NULL, jpf)) == NULL) {
        log("JD: can not find any physical ports");
        return NULL;
    }
    QStringList * ports = new QStringList();

    for (int i = 0; physicalPorts[i] != NULL; i++) {
        if (QString((isAudio ? JACK_DEFAULT_AUDIO_TYPE :
                     JACK_DEFAULT_MIDI_TYPE)) ==
                QString( jack_port_type
                         ( jack_port_by_name ( staticData_.client,
                                               physicalPorts[i]))))
        {
            ports->append(QString(physicalPorts[i]));
            log("JD: physical port found: " + QString(physicalPorts[i]));
        }
    }
    jack_free(physicalPorts);
    return ports;
}


int DriverJack::buffer_size() {return staticData_.bufferSize;}
int DriverJack::sample_rate() {return staticData_.sampleRate;}

int DriverJack::registerPort(Sample * buffer, bool isOutput, QString name)
{
    log("JD: registering port : <"+ name + ">...");
    if (staticData_.NPortsRegistered == MAX_N_PORTS ||
            staticData_.client == NULL ||
            buffer == NULL) {
        log("JD: can not register port");
        return -1;
    }
    int portID = -1;
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort == NULL &&
                staticData_.audioPortsList[i].buffer == NULL) {
            portID = i;
            break;
        }
    }
    if (portID == -1 ) {
        log("JD: number of ports exeeded maximum");
        return -1; // free record not found
    }

    if (name.isEmpty()) name =
            QString(isOutput ? AUDIO_OUT_PORT_PREFIX :
                               AUDIO_IN_PORT_PREFIX).arg(
                portID, N_DIGITS_IN_A_PORT_NAME,
                10, QChar('0'));

    jack_port_t * jackPort =
            jack_port_register (staticData_.client,
                                name.toLatin1().data(),
                                JACK_DEFAULT_AUDIO_TYPE,
                                (isOutput ? JackPortIsOutput :
                                            JackPortIsInput),
                                0);
    if (jackPort == NULL) {
        log("JD: port registration was rejected by jack server");
        return -1;
    }
    staticData_.NPortsRegistered++;
    staticData_.audioPortsList[portID].buffer = buffer;
    staticData_.audioPortsList[portID].isOutput = isOutput;
    staticData_.audioPortsList[portID].jackPort = jackPort;
    log("JD: port is registered : " + name);
    return portID;
}

QString DriverJack::setPortName(int portID, const QString & portName)
{
    if (staticData_.audioPortsList[portID].jackPort == NULL ||
            portName.isEmpty()) {
        log("JD: cant change portname to : " + portName);
        return "";
    }

    jack_port_set_name	(	staticData_.audioPortsList[portID].jackPort,
                            portName.toLatin1().data());
    log("JD: new port name is : " +  QString(jack_port_short_name(staticData_.audioPortsList[portID].jackPort)));
    return jack_port_short_name(staticData_.audioPortsList[portID].jackPort);
}

ReturnCode DriverJack::deletePort(const int portID)
{
    stopJack();
    if (jack_port_unregister (
                staticData_.client, staticData_.audioPortsList[portID].jackPort))	{
        delete [] staticData_.audioPortsList[portID].buffer;
        staticData_.audioPortsList[portID].buffer = NULL;
        staticData_.audioPortsList[portID].jackPort = NULL;
        startJack();
        return -1; //SUCCESS;
    }
    startJack();
    return -1; //JACK_CANT_UNREGISTER_PORT;
}

ReturnCode DriverJack::clearPorts ()
{
    if (driverState_ == STARTED) stopJack();
    for (int portID = 0; portID<MAX_N_PORTS; ++portID){
        if (staticData_.audioPortsList[portID].jackPort != NULL)
            if (jack_port_unregister (
                        staticData_.client, staticData_.audioPortsList[portID].jackPort))	{
                delete [] staticData_.audioPortsList[portID].buffer;
                staticData_.audioPortsList[portID].buffer = NULL;
                staticData_.audioPortsList[portID].jackPort = NULL;
            }
        //        startJack();
        staticData_.NPortsRegistered = 0;
    }
    return 0; //SUCCESS;
}


QStringList * DriverJack::getMyPortNames()
{
    QStringList * myPortNames = new QStringList();
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort != NULL) {
            myPortNames->append(
                        QString(jack_port_short_name(staticData_.audioPortsList[i].jackPort)));
        }
    }
    return myPortNames;
}

ReturnCode
DriverJack::connectPort( const int myPortID, const QString & extPortName ){
    if (driverState_ != STARTED ) { //JACK_CANT_CONNECT_PORT;
        log(QString(
                "JD: can't connect to port: " +
                extPortName + ", start driver first!"));
        return -1; }
    QString source;
    QString destination;
    if (staticData_.audioPortsList[myPortID].isOutput) {
        source = jack_port_name(staticData_.audioPortsList[myPortID].jackPort);
        destination = extPortName;
    } else {
        destination = jack_port_name(staticData_.audioPortsList[myPortID].jackPort);
        source = extPortName;
    }

    if (jack_connect (staticData_.client,source.toAscii().data(), destination.toAscii().data()) == 0)
    {
        log("JD: ports connected: " + source + " to: " + destination);
        return 0; //SUCCESS;
    } else {
        log("JD: can't connect " + source + " to: " + destination);
        return -1;//JACK_CANT_CONNECT_PORT;
    }
}

ReturnCode	DriverJack::connectDefaultPorts()
{
    // connecting first two inputs of the host to two physical outputs of the card
    // and vice versa, to provide basic operation mode
    log("JD: connecting defaults..");
    int nOutputs = 0;
    int nInputs = 0;
    QStringList * cardOuts = getPhysicalPortNames(false, true);// card's out = jackd's in
    QStringList * cardIns = getPhysicalPortNames(true, true);
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort != NULL) {
            jack_port_disconnect(staticData_.client, staticData_.audioPortsList[i].jackPort);
            if (staticData_.audioPortsList[i].isOutput) {
                if (nOutputs < 2 && (cardOuts->size() - nOutputs) > 0) {
                    connectPort(i, (*cardOuts)[nOutputs]);
                    nOutputs++;
                }
            }	else {
                if (nInputs < 2 && (cardIns->size() - nInputs) > 0) {
                    connectPort(i, (*cardIns)[nInputs]);
                    nInputs++;
                }
            }
        }
    }
    log("JD: connecting defaults done.");
    delete cardIns;
    delete cardOuts;
    return 0;//SUCCESS;
}
